#!/usr/bin/env python
# -*- coding= utf-8 -*-

"""A program to edit a pySlip text layer.

This version edits only an existing text layer.
"""


import sys
import json
import tkinter_error
try:
    import wx
    import wx.lib.newevent
except ImportError:
    msg = 'Sorry, you must install wxPython'
    tkinter_error.tkinter_error(msg)

# If we have log.py, well and good.  Otherwise ...
try:
    import log
    log = log.Log('pyslip.log', log.Log.DEBUG)
except ImportError:
    def log(*args, **kwargs):
        pass

import pyslip
import local_tiles


######
# Various demo constants
######

__version__ = '0.2'


# demo name/version
AppName = 'pySlip text layer editor %s' % __version__

# name of the layer file
LayerFilename = 'text_layer.json'

# initial view level and position
InitViewLevel = 2
InitViewPosition = (103.770, 1.335)

TileDirectory = 'tiles'
MinTileLevel = 1

# the number of decimal places in a lon/lat display
LonLatPrecision = 3

# startup size of the application
DefaultAppSize = (800, 665)

# how close click has to be before point is selected
# the value is distance squared (degrees^2)
PointSelectDelta = 0.025

######
# Various GUI layout constants
######

# sizes of various spacers
HSpacerSize = (3, 1)         # horizontal in application screen
VSpacerSize = (1, 5)         # vertical in control pane

# border width when packing GUI elements
PackBorder = 1


###############################################################################
# Override the wx.TextCtrl class to add read-only style and background colour
###############################################################################

class ROTextCtrl(wx.TextCtrl):
    """Override the wx.TextCtrl widget to get read-only text control which
    has a distinctive background colour."""

    # background colour for the 'read-only' text field
    ControlReadonlyColour = '#ffffcc'

    def __init__(self, parent, value, tooltip='', *args, **kwargs):
        wx.TextCtrl.__init__(self, parent, wx.ID_ANY, value=value,
                             style=wx.TE_READONLY, *args, **kwargs)
        self.SetBackgroundColour(ROTextCtrl.ControlReadonlyColour)
        self.SetToolTip(wx.ToolTip(tooltip))

###############################################################################
# Override the wx.StaticBox class to show our style
###############################################################################


class AppStaticBox(wx.StaticBox):

    def __init__(self, parent, label, *args, **kwargs):
        if label:
            label = '  ' + label + '  '
        wx.StaticBox.__init__(self, parent, wx.ID_ANY, label, *args, **kwargs)

###############################################################################
# An 'attributes' editor popup dialog
###############################################################################

(ChangeAttrEvent, EVT_CHANGEATTR) = wx.lib.newevent.NewEvent()


class EditTextPoint(wx.Dialog):
    # legal placement strings
    PlaceList = ['cc', 'nw', 'cn', 'ne', 'ce', 'se', 'cs', 'sw', 'cw']

    # GridBagSizer pack border value
    Border = 1

    # colour of text field 'in error'
    TextFieldErrorColour = '#ffcccc'

    # label layout flags
    LabelFlags = wx.TOP | wx.BOTTOM | wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL
    CtlFlags = wx.TOP | wx.BOTTOM | wx.EXPAND
    TxtFlags = 0

    # various colours
    TextFieldErrorColour = '#ffcccc'
    TextFieldOKColour = wx.WHITE

    # default attribute values
    DefaultLonValue = ''
    DefaultLatValue = ''
    DefaultLabelValue = ''
    DefaultPlaceValue = 'cc'
    DefaultXOffset = 0
    DefaultYOffset = 0
    DefaultColourValue = wx.BLACK
    DefaultSizeValue = 10

    def __init__(self, parent, data, title=None, **kwargs):
        """Initialize widget.

        parent   reference to owning widget
        data     attributes dictionary used to populate dialog
        kwargs   extra keyword args for super class
        """

        if title is None:
            title = 'Editor'

        self.parent = parent

        self.updating = True

        wx.Dialog.__init__(self, parent, wx.ID_ANY, title, **kwargs)
        self.SetBackgroundColour(wx.WHITE)

        # create GUI objects
        box = wx.StaticBox(self, wx.ID_ANY, ' Text Attributes ')
        sbs = wx.StaticBoxSizer(box, orient=wx.VERTICAL)
        gbs = wx.GridBagSizer(hgap=2, vgap=2)

        # longitude/latitude
        lab_lon = wx.StaticText(self, wx.ID_ANY, 'Lon:', style=self.TxtFlags)
        self.txt_lon = wx.TextCtrl(parent=self, id=wx.ID_ANY, value='')
        lab_lon.SetToolTip(wx.ToolTip('Longitude of text point'))
        self.txt_lon.SetToolTip(wx.ToolTip('Longitude of text point'))
        lab_lat = wx.StaticText(self, wx.ID_ANY, ' Lat:', style=self.TxtFlags)
        self.txt_lat = wx.TextCtrl(parent=self, id=wx.ID_ANY, value='')
        lab_lat.SetToolTip(wx.ToolTip('Latitude of text point'))
        self.txt_lat.SetToolTip(wx.ToolTip('Latitude of text point'))

        gbs.Add(lab_lon, pos=(0, 0), border=self.Border, flag=self.LabelFlags)
        gbs.Add(self.txt_lon, pos=(0, 1), border=self.Border,
                flag=self.CtlFlags)
        gbs.Add(lab_lat, pos=(0, 2), border=self.Border, flag=self.LabelFlags)
        gbs.Add(self.txt_lat, pos=(0, 3), border=self.Border,
                flag=self.CtlFlags)

        # spacer
        lab_label = wx.StaticText(self, wx.ID_ANY, 'Label:',
                                  style=self.TxtFlags)
        self.txt_label = wx.TextCtrl(parent=self, id=wx.ID_ANY, value='',
                                     size=wx.DefaultSize)
        lab_label.SetToolTip(wx.ToolTip('Label of text point'))
        self.txt_label.SetToolTip(wx.ToolTip('Label of text point'))

        gbs.Add(lab_label, (1, 0), border=self.Border, flag=self.LabelFlags)
        gbs.Add(self.txt_label, (1, 1), span=(1, 3), border=self.Border,
                flag=self.CtlFlags)

        # add the placement control
        place_text = wx.StaticText(self, wx.ID_ANY, 'Placement:',
                                   style=self.TxtFlags)
        self.cb_place = wx.ComboBox(self, -1, '', (15, 30), wx.DefaultSize,
                                    self.PlaceList,
                                    wx.CB_DROPDOWN | wx.CB_READONLY)
        self.cb_place.SetValue(self.PlaceList[0])
        place_text.SetToolTip(wx.ToolTip('Placement of label with '
                                         'respect to point'))
        self.cb_place.SetToolTip(wx.ToolTip('Placement of label with '
                                            'respect to point'))

        gbs.Add(place_text, pos=(2, 0), border=self.Border,
                flag=self.LabelFlags)
        gbs.Add(self.cb_place, pos=(2, 1), span=(1, 3), border=self.Border,
                flag=self.CtlFlags)

        # add the text offset attributes
        xoffset_text = wx.StaticText(self, wx.ID_ANY, 'Offset x:',
                                     style=self.TxtFlags)
        self.spc_xoffset = wx.SpinCtrl(self, -1, '', (30, 20), (80, -1))
        self.spc_xoffset.SetRange(-1000, 1000)
        self.spc_xoffset.SetValue(0)
        yoffset_text = wx.StaticText(self, wx.ID_ANY, ' y:',
                                     style=self.TxtFlags)
        self.spc_yoffset = wx.SpinCtrl(self, -1, '', (30, 20), (80, -1))
        self.spc_yoffset.SetRange(-1000, 1000)
        self.spc_yoffset.SetValue(0)
        ttip = wx.ToolTip('X offset of label with respect to point')
        xoffset_text.SetToolTip(ttip)
        ttip = wx.ToolTip('X offset of label with respect to point')
        self.spc_xoffset.SetToolTip(ttip)
        ttip = wx.ToolTip('Y offset of label with respect to point')
        yoffset_text.SetToolTip(ttip)
        ttip = wx.ToolTip('Y offset of label with respect to point')
        self.spc_yoffset.SetToolTip(ttip)

        gbs.Add(xoffset_text, pos=(3, 0), border=self.Border,
                flag=self.LabelFlags)
        gbs.Add(self.spc_xoffset, pos=(3, 1), border=self.Border,
                flag=self.CtlFlags)
        gbs.Add(yoffset_text, pos=(3, 2), border=self.Border,
                flag=self.LabelFlags)
        gbs.Add(self.spc_yoffset, pos=(3, 3), border=self.Border,
                flag=self.CtlFlags)

        # add the text colour attribute
        txt_label = wx.StaticText(self, wx.ID_ANY, 'Colour:',
                                  style=self.TxtFlags)
        self.cp_colour = wx.ColourPickerCtrl(self, id=wx.ID_ANY, col=wx.BLACK)
        self.cp_colour.SetInternalMargin(1)
        txt_label.SetToolTip(wx.ToolTip('Text colour'))
        self.cp_colour.SetToolTip(wx.ToolTip('Text colour'))

        gbs.Add(txt_label, pos=(4, 0), border=self.Border, flag=self.LabelFlags)
        gbs.Add(self.cp_colour, pos=(4, 1), span=(1, 3), border=self.Border,
                flag=self.CtlFlags)

        # add the text size attributes
        txt_label = wx.StaticText(self, wx.ID_ANY, 'Font size:',
                                  style=self.TxtFlags)
        self.spc_size = wx.SpinCtrl(self, -1, '', (30, 20), (80, -1))
        self.spc_size.SetRange(0, 1000)
        self.spc_size.SetValue(0)
        txt_label.SetToolTip(wx.ToolTip('Label font size'))
        self.spc_size.SetToolTip(wx.ToolTip('Label font size'))

        gbs.Add(txt_label, pos=(5, 0), border=self.Border, flag=self.LabelFlags)
        gbs.Add(self.spc_size, pos=(5, 1), span=(1, 3), border=self.Border,
                flag=self.CtlFlags)

        # finish layout
        sbs.Add(gbs)
        self.SetSizer(sbs)
        sbs.Fit(self)

        # set handler for CLOSE event and any attribute change
        self.Bind(wx.EVT_CLOSE, self.OnClose)

        self.txt_lon.Bind(wx.EVT_TEXT, self.OnChange)
        self.txt_lat.Bind(wx.EVT_TEXT, self.OnChange)
        self.txt_label.Bind(wx.EVT_TEXT, self.OnChange)
        self.cb_place.Bind(wx.EVT_COMBOBOX, self.OnChange)
        self.spc_xoffset.Bind(wx.EVT_SPINCTRL, self.OnChange)
        self.spc_yoffset.Bind(wx.EVT_SPINCTRL, self.OnChange)
        self.cp_colour.Bind(wx.EVT_COLOURPICKER_CHANGED, self.OnChange)
        self.spc_size.Bind(wx.EVT_SPINCTRL, self.OnChange)

        self.Update(data)

        self.updating = False

    def OnChange(self, event):
        """Validate attribute values and create change event."""

        # if change is because we are updating, ignore
        if self.updating:
            return

        # validate attribute values
        (error, lon) = self.ValidateLon(False)
        (error, lat) = self.ValidateLat(error)

        if error:
            return

        # get all other values (can't be in error)
        lab = self.txt_label.GetValue()
        placement = self.cb_place.GetValue()
        offset_x = self.spc_xoffset.GetValue()
        offset_y = self.spc_yoffset.GetValue()
        colour = self.cp_colour.GetColour()
        fontsize = self.spc_size.GetValue()

        # create 'changed' event, use pyslip attribute names
        attributes = {'lon': lon, 'lat': lat, 'label': lab,
                      'placement': placement, 'offset_x': offset_x,
                      'offset_y': offset_y, 'colour': colour,
                      'fontsize': fontsize}

        # raise a change event
        event = ChangeAttrEvent(attributes=attributes)
        event.attributes = attributes
        wx.PostEvent(self.parent, event)

    def OnClose(self, event):
        self.Destroy()

    def ValidateLon(self, error=False):
        """Validate the longitude value."""

        try:
            lon = float(self.txt_lon.GetValue())
            assert 0.0 <= lon <= 360.0
        except (AssertionError, ValueError):
            error = True
            lon = None
            self.txt_lon.SetBackgroundColour(self.TextFieldErrorColour)
        else:
            self.txt_lon.SetBackgroundColour(self.TextFieldOKColour)

        self.txt_lon.Update()

        return (error, lon)

    def ValidateLat(self, error=False):
        """Validate the latitude value."""

        try:
            lat = float(self.txt_lat.GetValue())
            assert -90.0 <= lat <= 90.0
        except (AssertionError, ValueError):
            error = True
            lat = None
            self.txt_lat.SetBackgroundColour(self.TextFieldErrorColour)
        else:
            self.txt_lat.SetBackgroundColour(self.TextFieldOKColour)

        self.txt_lat.Update()

        return (error, lat)

    def SetActive(self, state):
        """Set widget active state."""

        self.active = state
        if state:
            self.Enable()
        else:
            self.Disable()

    def Update(self, data):
        """Set attribute values.

        data   a dictionary of state values:
                   {'lon': lon, 'lat': lat, 'label': label,
                    'placement': place, 'offset_x': offset_x,
                    'offset_y': offset_y, 'colour': colour,
                    'fontsize': fontsize}
        """

        # update attribute values
        self.updating = True

        # set values from 'data' dictionary
        self.txt_lon.SetValue(data['lon'])
        self.txt_lat.SetValue(data['lat'])
        self.txt_label.SetValue(data['label'])
        self.cb_place.SetValue(data['placement'])
        self.spc_xoffset.SetValue(data['offset_x'])
        self.spc_yoffset.SetValue(data['offset_y'])
        self.cp_colour.SetColour(data['colour'])
        self.spc_size.SetValue(data['fontsize'])

        # enable everything
        self.updating = False


###############################################################################
# The main application frame
###############################################################################

class AppFrame(wx.Frame):
    place_list = ['cc', 'nw', 'cn', 'ne', 'ce', 'se', 'cs', 'sw', 'cw']

    def __init__(self, layer_filename):
        wx.Frame.__init__(self, None, size=DefaultAppSize, title=AppName)
        self.SetMinSize(DefaultAppSize)
        self.panel = wx.Panel(self, wx.ID_ANY)
        self.panel.SetBackgroundColour(wx.WHITE)
        self.panel.ClearBackground()

        # create the tile source object
        self.tile_source = local_tiles.LocalTiles(TileDirectory, None)

        # build the GUI
        self.make_gui(self.panel)

        self.layer_filename = layer_filename
        self.selected_point = None

        # do initialisation stuff - all the application stuff
        self.init()

        # finally, set up application window position
        self.Centre()

#####
# Build the GUI
#####

    def make_gui(self, parent):
        """Create application GUI."""

        # start application layout
        all_display = wx.BoxSizer(wx.HORIZONTAL)
        parent.SetSizer(all_display)

        # put map view in left of horizontal box
        sl_box = self.make_gui_view(parent)
        all_display.Add(sl_box, proportion=1, border=1, flag=wx.EXPAND)

        parent.SetSizerAndFit(all_display)

    def make_gui_view(self, parent):
        """Build the map view widget

        parent  reference to the widget parent

        Returns the static box sizer.
        """

        # create gui objects
        sb = AppStaticBox(parent, '')
        self.pyslip = pyslip.PySlip(parent, tile_src=self.tile_source,
                                    min_level=MinTileLevel,
                                    start_level=InitViewLevel)

        # lay out objects
        box = wx.StaticBoxSizer(sb, orient=wx.HORIZONTAL)
        box.Add(self.pyslip, proportion=1, border=1, flag=wx.EXPAND)

        return box

    def make_gui_controls(self, parent):
        """Build the 'controls' part of the GUI

        parent  reference to parent

        Returns reference to containing sizer object.
        """

        # all controls in vertical box sizer
        controls = wx.BoxSizer(wx.VERTICAL)

        # add the view info control
        view_info = self.make_view_info(parent)
        controls.Add(view_info, proportion=0, flag=wx.EXPAND | wx.ALL)

        # vertical spacer
        controls.AddSpacer(VSpacerSize)

        # put in the point attribute widget
        self.attributes = attributes.Attributes(parent)
        self.attributes.SetActive(False)
        controls.Add(self.attributes, proportion=0, flag=wx.EXPAND | wx.ALL)

        return controls

    def make_view_info(self, parent):
        """Build the control that shows the view information.

        parent  reference to parent

        Returns reference to containing sizer object.
        """

        # create objects
        txt_level = wx.StaticText(parent, wx.ID_ANY, 'Level: ')
        self.map_level = wx.StaticText(parent, wx.ID_ANY, ' ')
        txt_level.SetToolTip(wx.ToolTip('Shows the current map level'))
        self.map_level.SetToolTip(wx.ToolTip('Shows the current map level'))

        txt_lonlat = wx.StaticText(parent, wx.ID_ANY, 'Lon/Lat:')
        self.mouse_position = ROTextCtrl(parent, '', size=(150, -1),
                                         tooltip=('Shows the mouse '
                                                  'longitude and latitude '
                                                  'on the map'))
        txt_lonlat.SetToolTip(wx.ToolTip('Shows the mouse longitude and '
                                         'latitude on the map'))

        # lay out the controls
        sb = AppStaticBox(parent, 'View information')
        box = wx.StaticBoxSizer(sb, orient=wx.VERTICAL)
        hbox = wx.BoxSizer(orient=wx.HORIZONTAL)
        hbox.Add(txt_level, border=PackBorder,
                 flag=(wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT | wx.LEFT))
        hbox.Add(self.map_level, proportion=0, border=PackBorder,
                 flag=wx.RIGHT | wx.TOP)
        box.Add(hbox, flag=wx.EXPAND | wx.ALL)

        hbox = wx.BoxSizer(orient=wx.HORIZONTAL)
        hbox.Add(txt_lonlat, border=PackBorder,
                 flag=(wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT | wx.LEFT))
        hbox.Add(self.mouse_position, proportion=1, border=PackBorder,
                 flag=wx.RIGHT | wx.TOP | wx.BOTTOM)
        box.Add(hbox, flag=wx.EXPAND | wx.ALL)

        return box

    def make_gui_level(self, parent):
        """Build the control that shows the level.

        parent  reference to parent

        Returns reference to containing sizer object.
        """

        # create objects
        txt = wx.StaticText(parent, wx.ID_ANY, 'Level: ')
        self.map_level = wx.StaticText(parent, wx.ID_ANY, ' ')
        txt.SetToolTip(wx.ToolTip('Shows the current map level'))
        self.map_level.SetToolTip(wx.ToolTip('Shows the current map level'))

        # lay out the controls
        sb = AppStaticBox(parent, 'Map level')
        box = wx.StaticBoxSizer(sb, orient=wx.HORIZONTAL)
        box.Add(txt, border=PackBorder, flag=(wx.ALIGN_CENTER_VERTICAL
                                              | wx.ALIGN_RIGHT | wx.LEFT))
        box.Add(self.map_level, proportion=0, border=PackBorder,
                flag=wx.RIGHT | wx.TOP)

        return box

    def make_gui_mouse(self, parent):
        """Build the mouse part of the controls part of GUI.

        parent  reference to parent

        Returns reference to containing sizer object.
        """

        # create objects
        txt = wx.StaticText(parent, wx.ID_ANY, 'Lon/Lat:')
        self.mouse_position = ROTextCtrl(parent, '', size=(150, -1),
                                         tooltip=('Shows the mouse '
                                                  'longitude and latitude '
                                                  'on the map'))
        txt.SetToolTip(wx.ToolTip('Shows the mouse longitude and '
                                  'latitude on the map'))

        # lay out the controls
        sb = AppStaticBox(parent, 'Mouse position')
        box = wx.StaticBoxSizer(sb, orient=wx.HORIZONTAL)
        box.Add(txt, border=PackBorder,
                flag=(wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT | wx.LEFT))
        box.Add(self.mouse_position, proportion=1, border=PackBorder,
                flag=wx.RIGHT | wx.TOP | wx.BOTTOM)

        return box

    ######
    # pySlip text layer editor event handlers
    ######

    def pointSelect(self, event):
        """Handle point select exception from pyslip.

        event  the event that contains these attributes:
                   evtype  the type of event
                   point   selected point(s) geo coords+data
                               ((x,y), data)
                           (if None then no point(s) selected)
                   mposn   geo coords of actual mouse click point
        """

        self.click_point = event.mposn
        self.selected_point = None

        if event.evtype == pyslip.EventPointSelect:
            if event.point:
                (point, data) = event.point
            if event.point is None:
                # no point selected, turn off attributes widget
                self.point_index = None
                data_dict = {'lon': '', 'lat': '', 'lab': '', 'placement': '',
                             'offset_x': 0, 'offset_y': 0, 'colour': wx.WHITE,
                             'fontsize': 0}
                self.attributes.Update(False, data_dict)
            elif point:
                self.tooltip.Enable(True)

                # point was selected
                ((lon, lat), data) = event.point
                self.selected_point = (lon, lat)
                self.point_index = data
                text = self.TextData[data]
                (lon, lat, label, d) = text

                # update dictionary 'd' with default attribute vaues
                d['placement'] = d.get('placement',
                                       self.pyslip.DefaultTextPlacement)
                d['radius'] = d.get('fontname', self.pyslip.DefaultTextRadius)
                d['colour'] = d.get('colour', self.pyslip.DefaultTextColour)
                d['fontname'] = d.get('fontname',
                                      self.pyslip.DefaultTextFontname)
                d['fontsize'] = d.get('fontsize',
                                      self.pyslip.DefaultTextFontSize)
                d['offset_x'] = d.get('offset_x',
                                      self.pyslip.DefaultTextOffsetX)
                d['offset_y'] = d.get('offset_y',
                                      self.pyslip.DefaultTextOffsetY)

                # update the attributes widget
                data_dict = {'lon': '%.4f' % lon, 'lat': '%.4f' % lat,
                             'label': label, 'placement': d['placement'],
                             'offset_x': d.get('offset_x', 0),
                             'offset_y': d.get('offset_y', 0),
                             'colour': d.get('colour', wx.BLACK),
                             'fontsize': d.get('fontsize', 10)}

                self.attributes.Update(True, data_dict)
        elif event.evtype == pyslip.EventRightPointSelect:
            if event.point:
                (point, data) = event.point
            if event.point is None:
                # no point selected, display appropriate menu
                menu = self.makeNoPointMenu()
                self.pyslip.PopupMenu(menu, event.vposn)
                menu.Destroy()
            elif point:
                # save selected point
                self.selected_point = point
                self.point_index = data

                (lon, lat, label, d) = self.TextData[data]

                # update dictionary 'd' with default attribute vaues
                d['placement'] = d.get('placement',
                                       self.pyslip.DefaultTextPlacement)
                d['radius'] = d.get('radius', self.pyslip.DefaultTextRadius)
                d['colour'] = d.get('colour', self.pyslip.DefaultTextColour)
                d['fontname'] = d.get('fontname',
                                      self.pyslip.DefaultTextFontname)
                d['fontsize'] = d.get('fontsize',
                                      self.pyslip.DefaultTextFontSize)
                d['offset_x'] = d.get('offset_x',
                                      self.pyslip.DefaultTextOffsetX)
                d['offset_y'] = d.get('offset_y',
                                      self.pyslip.DefaultTextOffsetY)

                # update the attributes widget
                self.edit_point = {'lon': '%.4f' % lon, 'lat': '%.4f' % lat,
                                   'label': label}
                self.edit_point.update(d)

                # one point selected, show different menu
                menu = self.makeOnePointMenu()
                self.pyslip.PopupMenu(menu, event.vposn)
                menu.Destroy()

    def makeNoPointMenu(self):
        """Create a 'no point selected' popup menu."""

        menu = wx.Menu()

        item = menu.Append(wx.NewId(), 'New point')
        self.Bind(wx.EVT_MENU, self.MenuNewPoint, item)
        self.makeSystemMenu(menu)

        return menu

    def makeOnePointMenu(self):
        """Create a 'one point selected' popup menu."""

        menu = wx.Menu()

        item = menu.Append(wx.NewId(), 'New point')
        self.Bind(wx.EVT_MENU, self.MenuNewPoint, item)
        item = menu.Append(wx.NewId(), 'Edit point ...')
        self.Bind(wx.EVT_MENU, self.MenuEditPoint, item)
        item = menu.Append(wx.NewId(), 'Delete point')
        self.Bind(wx.EVT_MENU, self.MenuDeletePoint, item)
        self.makeSystemMenu(menu)

        return menu

    def makeManyPointsMenu(self, points):
        """Create a menu for multiple points.

        points  list of multi-point information

        Probably need to associate point data with Edit/Delete item.
        """

        menu = wx.Menu()

        item = menu.Append(wx.NewId(), 'New point')
        self.Bind(wx.EVT_MENU, self.MenuNewPoint, item)
        for point in points:
            item = menu.Append(wx.NewId(), 'Edit %s ...' % point.name)
            self.Bind(wx.EVT_MENU, self.MenuEditPoint, item)
        for point in points:
            item = menu.Append(wx.NewId(), 'Delete %s' % point.name)
            self.Bind(wx.EVT_MENU, self.MenuDeletePoint, item)
        self.makeSystemMenu(menu)

        return menu

    def makeSystemMenu(self, menu):
        """Create the SYSTEM MENU part of existing menu."""

        menu.AppendSeparator()
        item = menu.Append(wx.NewId(), 'Choose layer')
        self.Bind(wx.EVT_MENU, self.MenuChooseLayer, item)
        item = menu.Append(wx.NewId(), 'New layer')
        self.Bind(wx.EVT_MENU, self.MenuNewLayer, item)
        item = menu.Append(wx.NewId(), 'Delete layer')
        self.Bind(wx.EVT_MENU, self.MenuDeleteLayer, item)
        menu.AppendSeparator()
        item = menu.Append(wx.NewId(), 'Load layer')
        self.Bind(wx.EVT_MENU, self.MenuLoadLayer, item)
        item = menu.Append(wx.NewId(), 'Store layer')
        self.Bind(wx.EVT_MENU, self.MenuStoreLayer, item)
        item = menu.Append(wx.NewId(), 'Store layer as ...')
        self.Bind(wx.EVT_MENU, self.MenuStoreLayerAs, item)

    def MenuChooseLayer(self, event=None):
        self.not_yet()

    def MenuNewLayer(self, event=None):
        self.not_yet()

    def MenuDeleteLayer(self, event=None):
        self.not_yet()

    def MenuNewPoint(self, event):
        """Add a new point to layer.

        Add new point with defaults and put into attributes dialog.
        """

        (lon, lat) = self.click_point

        label = '<no label>'
        index = len(self.TextData)
        attributes = {'data': index}
        self.point_index = index
        new_p = [lon, lat, label, attributes]

        self.TextData.append(new_p)

        self.pyslip.DeleteLayer(self.text_layer)
        self.text_layer = self.pyslip.AddTextLayer(self.TextData, map_rel=True,
                                                   selectable=True)

        # update the attributes widget
        self.edit_point = {'lon': '%.4f' % lon, 'lat': '%.4f' % lat,
                           'label': label,
                           'placement': self.pyslip.DefaultTextPlacement,
                           'radius': self.pyslip.DefaultTextRadius,
                           'colour': self.pyslip.DefaultTextColour,
                           'fontname': self.pyslip.DefaultTextFontname,
                           'fontsize': self.pyslip.DefaultTextFontSize,
                           'offset_x': self.pyslip.DefaultTextOffsetX,
                           'offset_y': self.pyslip.DefaultTextOffsetY}

        edit_dialog = EditTextPoint(self, data=self.edit_point,
                                    title='Edit text point')
        edit_dialog.ShowModal()

    def MenuDeletePoint(self, event=None):
        """Remove a point from the layer."""

        (lon, lat) = self.click_point
        print('lon=%s, lat=%s' % (str(lon), str(lat)))

        self.not_yet()

    def MenuEditPoint(self, event=None):
        edit_dialog = EditTextPoint(self, data=self.edit_point,
                                    title='Edit text point')
        edit_dialog.ShowModal()

    def MenuLoadLayer(self, event=None):
        # open the resource
        with open('text_layer.json') as fp:
            layers = pyslip.Resource('text_layer.json')
            (ltype, ldata) = layers.GetLayer('test')
            if ltype != pyslip.PySlip.TypeText:
                msg = 'Wrong layer type, got %d' % ltype
                raise Exception(msg)

        # save any user data, add editor data
        self.user_data = {}
        for (idx, pt) in enumerate(ldata):
            (_, _, _, data) = pt
            try:
                self.user_data[idx] = data['data']
            except KeyError:
                pass

            data['data'] = idx

        self.TextData = ldata

        # add the text layer to pyslip
        self.text_layer = self.pyslip.AddTextLayer(ldata, map_rel=True,
                                                   selectable=True)

    def MenuStoreLayer(self, event=None):
        # put user data back into a *copy* of current layer
        save_layer = []
        for (idx, pt) in enumerate(self.TextData):
            (lon, lat, text, data) = pt
            del data['data']
            if idx in self.user_data:
                data['data'] = self.user_data[idx]
            save_layer.append([lon, lat, text, data])

        resource = pyslip.Resource()
        resource.AddLayer('test', self.pyslip.TypeText, save_layer)
        resource.Write('save.json')

#        if self.layer_filename:
#            resource = pyslip.Resource()
#            resource.AddLayer('test', self.pyslip.TypeText, self.TextData)
#            resource.Write(self.layer_filename)
#        else:
#            self.MenuStoreLayerAs(event)

    def MenuStoreLayerAs(self, event=None):
        self.not_yet()

    def MenuQuit(self, event=None):
        self.not_yet()

    def OnMenuItem(self, event=None):
        self.not_yet()

    def handle_position_event(self, event):
        """Handle a pySlip POSITION event."""

        posn_str = ''
        if event.position:
            (lon, lat) = event.position
            posn_str = ('%.*f / %.*f'
                        % (LonLatPrecision, lon, LonLatPrecision, lat))

        self.mouse_position.SetValue(posn_str)

    def handle_level_change(self, event):
        """Handle a pySlip LEVEL event."""

        self.map_level.SetLabel('%d' % event.level)

    ######
    # Finish initialization of data, etc
    ######

    def init(self):
        # load initial empty layer
        self.TextData = []    # no text layer at first
        self.text_layer = self.pyslip.AddTextLayer(self.TextData, map_rel=True,
                                                   selectable=False)

        self.MenuLoadLayer()    #?

        # finally, bind events to handlers
        self.Bind(pyslip.EVT_PYSLIP_SELECT, self.handle_select_event)
        self.Bind(EVT_CHANGEATTR, self.save_attributes)

        # define instance variables
        self.point_index = None		# index of point we are editing

        # force pyslip initialisation
        self.pyslip.OnSize()

    ######
    # Exception handlers
    ######

    def handle_select_event(self, event):
        """Handle a pySlip point/box SELECT event."""

        if event.evtype == pyslip.EventRightPointSelect:
            self.pointSelect(event)

    def null_handler(self, event):
        """Routine to handle unexpected events."""

        log.info('null_handler: event=%s' % type(event))
        pass

    def save_attributes(self, event):
        """Handle an Attribute 'change' event.

        event  event containing attribute values
        """

        attributes = event.attributes

        # convert various attributes into form json can handle
        attributes['colour'] = \
            attributes['colour'].GetAsString(flags=wx.C2S_HTML_SYNTAX)

        # update the text layer point with data == self.point_index
        # then remove displayed layer and add new one
        new_layer = []
        for p in self.TextData:
            (_, _, _, attr) = p
            if attr['data'] == self.point_index:
                lon = attributes['lon']
                del attributes['lon']
                lat = attributes['lat']
                del attributes['lat']
                label = attributes['label']
                del attributes['label']
                attributes['data'] = self.point_index
                new_p = [lon, lat, label, attributes]
                new_layer.append(new_p)
            else:
                new_layer.append(p)

        self.pyslip.DeleteLayer(self.text_layer)
        self.text_layer = self.pyslip.AddTextLayer(new_layer, map_rel=True,
                                                   selectable=True)
        self.TextData = new_layer

    def not_yet(self):
        tkinter_error.tkinter_error('Not yet implemented', title='Not yet')


###############################################################################

class MyApp(wx.App):
    def OnInit(self):
        self.frame = AppFrame(LayerFilename)
        self.SetTopWindow(self.frame)
        self.frame.Show()
        return True

###############################################################################

if __name__ == '__main__':
    import traceback
    import tkinter_error

    # our own handler for uncaught exceptions
    def excepthook(type, value, tb):
        msg = '\n' + '=' * 80
        msg += '\nUncaught exception:\n'
        msg += ''.join(traceback.format_exception(type, value, tb))
        msg += '=' * 80 + '\n'
        log(msg)
        tkinter_error.tkinter_error(msg)
        sys.exit(1)

    # plug our handler into the python system
    sys.excepthook = excepthook

    # start wxPython app
    app = MyApp(False)

#    import wx.lib.inspection
#    wx.lib.inspection.InspectionTool().Show()
    app.MainLoop()

